SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
4 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
4.1 KB · 40 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { getProperty } from "@/lib/queries";5import { dbAvailable } from "@/lib/db";6import { fmtPrice, fmtInt, fmtArea } from "@/lib/format";7import { CATEGORY_LABEL } from "@/lib/normalize";8import { ListingCardView } from "@/components/listing-card";9import { MapView } from "@/components/map-view";10import { Notice, Stat } from "@/components/ui";11export const dynamic = "force-dynamic";12type Props = { params: Promise<{ id: string }> };13export async function generateMetadata({ params }: Props): Promise<Metadata> { const { id } = await params; const p = dbAvailable() ? getProperty(id) : null; return { title: p ? `${p.address ?? "Propriété"}, ${p.city} — fiche propriété` : "Propriété", robots: { index: false } }; }14export default async function Propriete({ params }: Props) {15  const { id } = await params; if (!dbAvailable()) notFound();16  const p = getProperty(id); if (!p) notFound();17  const sale = p.listings.filter((l) => l.market === "sale"); const rent = p.listings.filter((l) => l.market === "rent");18  const byType = new Map<string, typeof rent>(); for (const l of rent) { const k = l.typeLabel ?? "Logement"; byType.set(k, [...(byType.get(k) ?? []), l]); }19  return (20    <article className="mx-auto max-w-7xl px-4 py-6">21      <p className="tv-label">Fiche propriété canonique</p>22      <h1 className="text-2xl md:text-3xl font-extrabold tracking-tight mt-1">{p.address ?? CATEGORY_LABEL[p.category ?? "autre"]}</h1>23      <p className="text-ink-2">{p.city}{p.region ? `, ${p.region}` : ""}</p>24      <div className="mt-4 grid sm:grid-cols-4 gap-3">25        <Stat label="Annonces reliées" value={fmtInt(p.n_listings)} sub={`${p.n_sale} à vendre · ${p.n_rent} à louer`} />26        <Stat label="Rapprochement" value={p.key_kind === "role" ? "Rôle" : p.key_kind === "address" ? "Adresse" : "Coordonnées"} sub={`confiance ${Math.round((p.dedup_confidence ?? 0) * 100)} %`} />27        {p.role_value != null && <Stat label="Valeur au rôle" value={fmtPrice(p.role_value)} sub={`terrain ${fmtPrice(p.role_land)} · bâtiment ${fmtPrice(p.role_building)}`} />}28        {(p.area_sqft || p.year_built) && <Stat label="Attributs" value={p.area_sqft ? fmtArea(p.area_sqft) : "—"} sub={[p.bedrooms != null ? `${p.bedrooms} ch.` : null, p.bathrooms != null ? `${p.bathrooms} sdb` : null, p.year_built ? `construite en ${p.year_built}` : null].filter(Boolean).join(" · ")} />}29      </div>30      <Notice>Une propriété peut avoir plusieurs annonces (sources, unités, dates). Toveli les relie par {p.key_kind === "role" ? "l'unité du rôle d'évaluation (jumelage spatial ≤ 40 m)" : p.key_kind === "address" ? "une clé d'adresse normalisée (numéro civique + rue + municipalité)" : "une cellule de coordonnées (≈ 10 m)"} ; les unités en copropriété et les adresses ambiguës ne sont pas fusionnées automatiquement.</Notice>31      {p.lat != null && p.lng != null && <div className="mt-4 h-64 rounded-card overflow-hidden border border-line"><MapView points={p.listings.filter((l) => l.lat != null).slice(0, 1).map((l) => ({ uid: l.uid, lat: l.lat!, lng: l.lng!, price: l.price ?? 0 }))} center={[p.lat, p.lng]} zoom={15} /></div>}32      {sale.length > 0 && <section className="mt-8"><h2 className="text-xl font-bold mb-3">À vendre ({sale.length})</h2><div className="grid grid-cols-2 md:grid-cols-4 gap-3">{sale.map((l) => <ListingCardView key={l.uid} c={l} />)}</div></section>}33      {rent.length > 0 && <section className="mt-8"><h2 className="text-xl font-bold mb-3">À louer ({rent.length})</h2>34        {[...byType.entries()].map(([t, ls]) => <div key={t} className="mb-5"><h3 className="font-semibold text-ink-2 mb-2">{t} · {ls.length}</h3><div className="grid grid-cols-2 md:grid-cols-4 gap-3">{ls.slice(0, 12).map((l) => <ListingCardView key={l.uid} c={l} />)}</div>{ls.length > 12 && <p className="text-xs text-ink-3 mt-2">+ {ls.length - 12} autres unités de ce type.</p>}</div>)}35      </section>}36      <p className="mt-8 text-sm"><Link href="/recherche" className="font-semibold text-brand-dark hover:underline">← Retour à la recherche</Link></p>37    </article>38  );39}40